/*
 * Created on 14-Jun-2004
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.net.udp.mc.impl;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AEThread;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HostNameToIPResolver;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;

import com.aelitis.azureus.core.util.NetUtils;
import com.aelitis.net.udp.mc.MCGroup;
import com.aelitis.net.udp.mc.MCGroupAdapter;
import com.aelitis.net.udp.mc.MCGroupException;

/**
 * @author parg
 * 
 */

public class MCGroupImpl implements MCGroup {
    private final static int TTL = 4;

    private final static int PACKET_SIZE = 8192;

    private static boolean overall_suspended;
    private static Map<String, MCGroupImpl> singletons = new HashMap<String, MCGroupImpl>();

    private static AEMonitor class_mon = new AEMonitor("MCGroup:class");

    private static AsyncDispatcher async_dispatcher = new AsyncDispatcher();

    public static MCGroupImpl getSingleton(MCGroupAdapter adapter, String group_address, int group_port, int control_port, String[] interfaces)

    throws MCGroupException {
        try {
            class_mon.enter();

            String key = group_address + ":" + group_port + ":" + control_port;

            MCGroupImpl singleton = (MCGroupImpl) singletons.get(key);

            if (singleton == null) {

                if (control_port == 0) {

                    int last_allocated = COConfigurationManager.getIntParameter("mcgroup.ports." + key, 0);

                    if (last_allocated != 0) {

                        try {
                            DatagramSocket test_socket = new DatagramSocket(null);

                            test_socket.setReuseAddress(false);

                            test_socket.bind(new InetSocketAddress(last_allocated));

                            test_socket.close();

                            control_port = last_allocated;

                        } catch (Throwable e) {

                            e.printStackTrace();
                        }
                    }
                }

                singleton = new MCGroupImpl(adapter, group_address, group_port, control_port, interfaces, overall_suspended);

                if (control_port == 0) {

                    control_port = singleton.getControlPort();

                    COConfigurationManager.setParameter("mcgroup.ports." + key, control_port);
                }

                singletons.put(key, singleton);
            }

            return (singleton);

        } finally {

            class_mon.exit();
        }
    }

    public static void setSuspended(boolean suspended) {
        try {
            class_mon.enter();

            if (overall_suspended == suspended) {

                return;
            }

            overall_suspended = suspended;

            for (MCGroupImpl group : singletons.values()) {

                group.setInstanceSuspended(overall_suspended);
            }
        } finally {

            class_mon.exit();
        }
    }

    private MCGroupAdapter adapter;

    private String group_address_str;
    private int group_port;
    private int control_port;
    protected InetSocketAddress group_address;
    private String[] selected_interfaces;

    private boolean ttl_problem_reported = true; // remove these diagnostic reports on win98
    private boolean sso_problem_reported = true; // remove these diagnostic reports on win98

    protected AEMonitor this_mon = new AEMonitor("MCGroup");

    private Map<String, Set<InetAddress>> current_registrations = new HashMap<String, Set<InetAddress>>();

    private volatile boolean instance_suspended;
    private List<Object[]> suspended_threads = new ArrayList<Object[]>();

    private MCGroupImpl(MCGroupAdapter _adapter, String _group_address, int _group_port, int _control_port, String[] _interfaces,
            boolean _is_suspended)

    throws MCGroupException {
        adapter = _adapter;

        group_address_str = _group_address;
        group_port = _group_port;
        control_port = _control_port;
        selected_interfaces = _interfaces;

        instance_suspended = _is_suspended;

        try {
            InetAddress ia = HostNameToIPResolver.syncResolve(group_address_str);

            group_address = new InetSocketAddress(ia, 0);

            processNetworkInterfaces(true);

            SimpleTimer.addPeriodicEvent("MCGroup:refresher", 60 * 1000, new TimerEventPerformer() {
                public void perform(TimerEvent event) {
                    try {
                        processNetworkInterfaces(false);

                    } catch (Throwable e) {

                        adapter.log(e);
                    }
                }
            });

        } catch (Throwable e) {

            throw (new MCGroupException("Failed to initialise MCGroup", e));
        }
    }

    private void setInstanceSuspended(boolean _suspended) {
        try {
            this_mon.enter();

            if (instance_suspended == _suspended) {

                return;
            }

            instance_suspended = _suspended;

            if (!instance_suspended) {

                List<Object[]> states = new ArrayList<Object[]>(suspended_threads);

                suspended_threads.clear();

                for (final Object[] state : states) {

                    new AEThread2((String) state[0], true) {
                        public void run() {
                            handleSocket((NetworkInterface) state[1], (InetAddress) state[2], (DatagramSocket) state[3], (Boolean) state[4]);
                        }
                    }.start();
                }
            }
        } finally {

            this_mon.exit();
        }

        if (!_suspended) {

            async_dispatcher.dispatch(new AERunnable() {
                public void runSupport() {
                    try {
                        processNetworkInterfaces(false);

                    } catch (Throwable e) {

                        adapter.log(e);
                    }
                }
            });
        }
    }

    private void processNetworkInterfaces(boolean start_of_day)

    throws SocketException {
        Map<String, Set<InetAddress>> new_registrations = new HashMap<String, Set<InetAddress>>();

        Map<String, NetworkInterface> changed_interfaces = new HashMap<String, NetworkInterface>();

        try {
            this_mon.enter();

            if (instance_suspended) {

                return;
            }

            List<NetworkInterface> x = NetUtils.getNetworkInterfaces();

            for (final NetworkInterface network_interface : x) {

                if (!interfaceSelected(network_interface)) {

                    if (start_of_day) {

                        adapter.trace("ignoring interface " + network_interface.getName() + ":" + network_interface.getDisplayName()
                                + ", not selected");
                    }

                    continue;
                }

                String ni_name = network_interface.getName();

                Set<InetAddress> old_address_set = current_registrations.get(ni_name);

                if (old_address_set == null) {

                    old_address_set = new HashSet<InetAddress>();
                }

                Set<InetAddress> new_address_set = new HashSet<InetAddress>();

                new_registrations.put(ni_name, new_address_set);

                Enumeration<InetAddress> ni_addresses = network_interface.getInetAddresses();

                while (ni_addresses.hasMoreElements()) {

                    final InetAddress ni_address = ni_addresses.nextElement();

                    new_address_set.add(ni_address);

                    if (old_address_set.contains(ni_address)) {

                        // already established

                        continue;
                    }
                    // turn on loopback to see if it helps for local host UPnP devices
                    // nah, turn it off again, it didn;t

                    if (ni_address.isLoopbackAddress()) {

                        if (start_of_day) {

                            adapter.trace("ignoring loopback address " + ni_address + ", interface " + network_interface.getName());
                        }

                        continue;
                    }

                    if (ni_address instanceof Inet6Address) {

                        if (start_of_day) {

                            adapter.trace("ignoring IPv6 address " + ni_address + ", interface " + network_interface.getName());
                        }

                        continue;
                    }

                    if (!start_of_day) {

                        if (!changed_interfaces.containsKey(ni_name)) {

                            changed_interfaces.put(ni_name, network_interface);
                        }
                    }

                    try {
                        // set up group

                        final MulticastSocket mc_sock = new MulticastSocket(group_port);

                        mc_sock.setReuseAddress(true);

                        // windows 98 doesn't support setTimeToLive

                        try {
                            mc_sock.setTimeToLive(TTL);

                        } catch (Throwable e) {

                            if (!ttl_problem_reported) {

                                ttl_problem_reported = true;

                                adapter.log(e);
                            }
                        }

                        String addresses_string = "";

                        Enumeration<InetAddress> it = network_interface.getInetAddresses();

                        while (it.hasMoreElements()) {

                            InetAddress addr = it.nextElement();

                            addresses_string += (addresses_string.length() == 0 ? "" : ",") + addr;
                        }

                        adapter.trace("group = " + group_address + "/" + network_interface.getName() + ":" + network_interface.getDisplayName()
                                + "-" + addresses_string + ": started");

                        mc_sock.joinGroup(group_address, network_interface);

                        mc_sock.setNetworkInterface(network_interface);

                        // note that false ENABLES loopback mode which is what we want

                        mc_sock.setLoopbackMode(false);

                        Runtime.getRuntime().addShutdownHook(new AEThread("MCGroup:VMShutdown") {
                            public void runSupport() {
                                try {
                                    mc_sock.leaveGroup(group_address, network_interface);

                                } catch (Throwable e) {

                                    adapter.log(e);
                                }
                            }
                        });

                        new AEThread2("MCGroup:MCListener", true) {
                            public void run() {
                                handleSocket(network_interface, ni_address, mc_sock, true);
                            }
                        }.start();

                    } catch (Throwable e) {

                        adapter.log(e);
                    }

                    // now do the incoming control listener

                    try {
                        final DatagramSocket control_socket = new DatagramSocket(null);

                        control_socket.setReuseAddress(true);

                        control_socket.bind(new InetSocketAddress(ni_address, control_port));

                        if (control_port == 0) {

                            control_port = control_socket.getLocalPort();

                            // System.out.println( "local port = " + control_port );
                        }

                        new AEThread2("MCGroup:CtrlListener", true) {
                            public void run() {
                                handleSocket(network_interface, ni_address, control_socket, false);
                            }
                        }.start();

                    } catch (Throwable e) {

                        adapter.log(e);
                    }
                }
            }
        } finally {

            current_registrations = new_registrations;

            this_mon.exit();
        }

        for (NetworkInterface ni : changed_interfaces.values()) {

            try {
                adapter.interfaceChanged(ni);

            } catch (Throwable e) {

                Debug.out(e);
            }
        }
    }

    public int getControlPort() {
        return (control_port);
    }

    private boolean interfaceSelected(NetworkInterface ni) {
        if (selected_interfaces != null && selected_interfaces.length > 0) {

            boolean ok = false;

            for (int i = 0; i < selected_interfaces.length; i++) {

                if (ni.getName().equalsIgnoreCase(selected_interfaces[i])) {

                    ok = true;

                    break;
                }
            }

            return (ok);

        } else {

            return (true);
        }
    }

    private boolean validNetworkAddress(final NetworkInterface network_interface, final InetAddress ni_address) {
        String ni_name = network_interface.getName();

        try {
            this_mon.enter();

            Set<InetAddress> set = (Set<InetAddress>) current_registrations.get(ni_name);

            if (set == null) {

                return (false);
            }

            return (set.contains(ni_address));

        } finally {

            this_mon.exit();
        }
    }

    public void sendToGroup(final byte[] data) {
        if (instance_suspended) {

            return;
        }

        // have debugs showing the send-to-group operation hanging and blocking AZ close, make async

        async_dispatcher.dispatch(new AERunnable() {
            public void runSupport() {
                sendToGroupSupport(data);
            }
        });
    }

    private void sendToGroupSupport(byte[] data) {
        try {
            List<NetworkInterface> x = NetUtils.getNetworkInterfaces();

            for (final NetworkInterface network_interface : x) {

                if (!interfaceSelected(network_interface)) {

                    continue;
                }

                Enumeration<InetAddress> ni_addresses = network_interface.getInetAddresses();

                boolean ok = false;

                while (ni_addresses.hasMoreElements()) {

                    InetAddress ni_address = ni_addresses.nextElement();

                    if (!(ni_address instanceof Inet6Address || ni_address.isLoopbackAddress())) {

                        ok = true;

                        break;
                    }
                }

                if (!ok) {

                    continue;
                }

                try {

                    MulticastSocket mc_sock = new MulticastSocket(null);

                    mc_sock.setReuseAddress(true);

                    try {
                        mc_sock.setTimeToLive(TTL);

                    } catch (Throwable e) {

                        if (!ttl_problem_reported) {

                            ttl_problem_reported = true;

                            adapter.log(e);
                        }
                    }

                    mc_sock.bind(new InetSocketAddress(control_port));

                    mc_sock.setNetworkInterface(network_interface);

                    // System.out.println( "sendToGroup: ni = " + network_interface.getName() + ", data = " + new String(data));

                    DatagramPacket packet = new DatagramPacket(data, data.length, group_address.getAddress(), group_port);

                    mc_sock.send(packet);

                    mc_sock.close();

                } catch (Throwable e) {

                    if (!sso_problem_reported) {

                        sso_problem_reported = true;

                        adapter.log(e);
                    }
                }
            }
        } catch (Throwable e) {
        }
    }

    public void sendToGroup(final String param_data) {
        if (instance_suspended) {

            return;
        }

        // have debugs showing the send-to-group operation hanging and blocking AZ close, make async

        async_dispatcher.dispatch(new AERunnable() {
            public void runSupport() {
                sendToGroupSupport(param_data);
            }
        });
    }

    private void sendToGroupSupport(String param_data) {
        try {
            List<NetworkInterface> x = NetUtils.getNetworkInterfaces();

            for (NetworkInterface network_interface : x) {

                if (!interfaceSelected(network_interface)) {

                    continue;
                }

                Enumeration<InetAddress> ni_addresses = network_interface.getInetAddresses();

                InetAddress an_address = null;

                while (ni_addresses.hasMoreElements()) {

                    InetAddress ni_address = ni_addresses.nextElement();

                    if (!(ni_address instanceof Inet6Address || ni_address.isLoopbackAddress())) {

                        an_address = ni_address;

                        break;
                    }
                }

                if (an_address == null) {

                    continue;
                }

                try {

                    MulticastSocket mc_sock = new MulticastSocket(null);

                    mc_sock.setReuseAddress(true);

                    try {
                        mc_sock.setTimeToLive(TTL);

                    } catch (Throwable e) {

                        if (!ttl_problem_reported) {

                            ttl_problem_reported = true;

                            adapter.log(e);
                        }
                    }

                    mc_sock.bind(new InetSocketAddress(control_port));

                    mc_sock.setNetworkInterface(network_interface);

                    byte[] data = param_data.replaceAll("%AZINTERFACE%", an_address.getHostAddress()).getBytes();

                    // System.out.println( "sendToGroup: ni = " + network_interface.getName() + ", data = " + new String(data));

                    DatagramPacket packet = new DatagramPacket(data, data.length, group_address.getAddress(), group_port);

                    mc_sock.send(packet);

                    mc_sock.close();

                } catch (Throwable e) {

                    if (!sso_problem_reported) {

                        sso_problem_reported = true;

                        adapter.log(e);
                    }
                }
            }
        } catch (Throwable e) {
        }
    }

    private void handleSocket(final NetworkInterface network_interface, final InetAddress local_address, final DatagramSocket socket,
            final boolean log_on_stop) {
        long successful_accepts = 0;
        long failed_accepts = 0;

        int port = socket.getLocalPort();

        try {
            // introduce a timeout so that when a Network interface changes we don't sit here
            // blocking forever and thus never realise that we should shutdown

            socket.setSoTimeout(30000);

        } catch (Throwable e) {

        }

        while (true) {

            if (instance_suspended) {

                try {
                    this_mon.enter();

                    if (instance_suspended) {

                        suspended_threads.add(new Object[] { Thread.currentThread().getName(), network_interface, local_address, socket,
                                log_on_stop });

                        return;
                    }
                } finally {

                    this_mon.exit();
                }
            }
            if (!validNetworkAddress(network_interface, local_address)) {

                if (log_on_stop) {

                    adapter.trace("group = " + group_address + "/" + network_interface.getName() + ":" + network_interface.getDisplayName() + " - "
                            + local_address + ": stopped");
                }

                return;
            }

            try {
                byte[] buf = new byte[PACKET_SIZE];

                DatagramPacket packet = new DatagramPacket(buf, buf.length);

                socket.receive(packet);

                successful_accepts++;

                failed_accepts = 0;

                receivePacket(network_interface, local_address, packet);

            } catch (SocketTimeoutException e) {

            } catch (Throwable e) {

                failed_accepts++;

                adapter.trace("MCGroup: receive failed on port " + port + ":" + e.getMessage());

                if ((failed_accepts > 100 && successful_accepts == 0) || failed_accepts > 1000) {

                    adapter.trace("    too many failures, abandoning");

                    break;
                }
            }
        }
    }

    private void receivePacket(NetworkInterface network_interface, InetAddress local_address, DatagramPacket packet) {
        if (instance_suspended) {

            return;
        }

        byte[] data = packet.getData();
        int len = packet.getLength();

        // System.out.println( "receive: add = " + local_address + ", data = " + new String( data, 0, len ));

        adapter.received(network_interface, local_address, (InetSocketAddress) packet.getSocketAddress(), data, len);
    }

    public void sendToMember(InetSocketAddress address, byte[] data)

    throws MCGroupException {
        if (instance_suspended) {

            return;
        }

        DatagramSocket reply_socket = null;

        // System.out.println( "sendToMember: add = " + address + ", data = " +new String( data ));

        try {
            reply_socket = new DatagramSocket(null);

            reply_socket.setReuseAddress(true);

            reply_socket.bind(new InetSocketAddress(group_port));

            DatagramPacket reply_packet = new DatagramPacket(data, data.length, address);

            reply_socket.send(reply_packet);

        } catch (Throwable e) {

            throw (new MCGroupException("sendToMember failed", e));

        } finally {

            if (reply_socket != null) {

                try {
                    reply_socket.close();

                } catch (Throwable e) {
                }
            }
        }
    }
}
